package main

import (
	"notabug.org/apiote/amuse/accounts"
	"notabug.org/apiote/amuse/db"
	"notabug.org/apiote/amuse/front"
	"notabug.org/apiote/amuse/libamuse"
	"notabug.org/apiote/amuse/network"
	"notabug.org/apiote/amuse/config"

	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"mime"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"
)

func person(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	etag := r.Header.Get("Etag")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	auth := getAuthToken(r)

	defer recovery(acceptLanguages, mimetype, w)

	path := strings.Split(r.URL.Path[1:], "/")
	_, err := strconv.ParseInt(path[1], 10, 64)
	if err != nil {
		renderError(400, w, err, acceptLanguages, mimetype)
		return
	} else if len(path) > 2 {
		renderError(404, w, nil, acceptLanguages, mimetype)
		return
	}
	person, err := libamuse.ShowPerson(path[1], etag, acceptLanguages, mimetype, auth)
	render(person, err, w, acceptLanguages, mimetype)
}

func tvSerie(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	etag := r.Header.Get("Etag")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	auth := getAuthToken(r)

	defer recovery(acceptLanguages, mimetype, w)

	path := strings.Split(r.URL.Path[1:], "/")
	_, err := strconv.ParseInt(path[1], 10, 64)
	if err != nil {
		renderError(400, w, err, acceptLanguages, mimetype)
		return
	} else if len(path) > 2 {
		renderError(404, w, nil, acceptLanguages, mimetype)
		return
	}
	tvSerie, err := libamuse.ShowTvSerie(path[1], etag, acceptLanguages, mimetype, auth)
	render(tvSerie, err, w, acceptLanguages, mimetype)
}

func film(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	auth := getAuthToken(r)

	defer recovery(acceptLanguages, mimetype, w)

	path := strings.Split(r.URL.Path[1:], "/")
	_, err := strconv.ParseInt(path[1], 10, 64)
	if err != nil {
		renderError(400, w, err, acceptLanguages, mimetype)
		return
	} else if len(path) > 2 {
		renderError(404, w, nil, acceptLanguages, mimetype)
		return
	}
	film, err := libamuse.ShowFilm(path[1], acceptLanguages, mimetype, auth)
	render(film, err, w, acceptLanguages, mimetype)
}

func book(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	auth := getAuthToken(r)

	defer recovery(acceptLanguages, mimetype, w)
	path := strings.Split(r.URL.Path[1:], "/")
	if len(path) > 2 {
		renderError(404, w, nil, acceptLanguages, mimetype)
		return
	}
	book, err := libamuse.ShowBook(path[1], acceptLanguages, mimetype, auth)
	render(book, err, w, acceptLanguages, mimetype)
}

func bookSerie(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	auth := getAuthToken(r)

	defer recovery(acceptLanguages, mimetype, w)
	path := strings.Split(r.URL.Path[1:], "/")
	if len(path) > 2 {
		renderError(404, w, nil, acceptLanguages, mimetype)
		return
	}
	bookSerie, err := libamuse.ShowBookSerie(path[1], acceptLanguages, mimetype, auth)
	render(bookSerie, err, w, acceptLanguages, mimetype)
}

func search(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	auth := getAuthToken(r)

	defer recovery(acceptLanguages, mimetype, w)

	path := strings.Split(r.URL.Path[1:], "/")
	if len(path) > 2 {
		renderError(404, w, nil, acceptLanguages, mimetype)
		return
	}
	query := r.URL.Query().Get("q")
	page := r.URL.Query().Get("page")
	results, err := libamuse.PerformSearch(query, acceptLanguages, mimetype, page, auth)
	render(results, err, w, acceptLanguages, mimetype)
}

func index(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	auth := getAuthToken(r)

	defer recovery(acceptLanguages, mimetype, w)

	path := strings.Split(r.URL.Path[1:], "/")
	if path[0] != "" {
		renderError(404, w, nil, acceptLanguages, mimetype)
		return
	}
	index, err := libamuse.ShowIndex(acceptLanguages, mimetype, auth)
	render(index, err, w, acceptLanguages, mimetype)
}

func about(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	auth := getAuthToken(r)

	defer recovery(acceptLanguages, mimetype, w)

	about, err := libamuse.ShowAbout(acceptLanguages, mimetype, auth)
	render(about, err, w, acceptLanguages, mimetype)
}

func loginGet(w http.ResponseWriter, r *http.Request, acceptLanguages, mimetype string) {
	referer := r.Header.Get("Referer")
	target := getTarget(referer, r.Host)
	if target == "/signedup" {
		target = "/"
	}
	auth := getAuthToken(r)
	user, _ := libamuse.VerifyAuthToken(auth)
	if !user.IsEmpty() {
		w.Header().Add("Location", target)
		w.WriteHeader(303)
		return
	}
	login, err := libamuse.ShowLogin(acceptLanguages, mimetype, nil, target)
	render(login, err, w, acceptLanguages, mimetype)
}

func loginPost(w http.ResponseWriter, r *http.Request, acceptLanguages, mimetype string) {
	// todo check mimetype (html,capnproto)
	r.ParseForm()
	username := r.PostForm.Get("username")
	password := r.PostForm.Get("password")
	target := r.PostForm.Get("target")
	if target == "" {
		target = "/"
	}
	sfa := r.PostForm.Get("sfa")
	remember := r.PostForm.Get("remember") == "true"

	token, err := libamuse.DoLogin(username, password, sfa, remember)
	if err != nil {
		fmt.Println(err)
		if authErr, ok := err.(accounts.AuthError); ok {
			var login string
			var err error
			if mimetype == "text/html" {
				login, err = libamuse.ShowLogin(acceptLanguages, mimetype, &authErr, target)
			} else {
				// todo send capnproto not authed
			}
			render(login, err, w, acceptLanguages, mimetype)
		} else {
			render("", err, w, acceptLanguages, mimetype)
		}
	} else {
		if mimetype == "text/html" {
			setAuthCookie(remember, token, w)
			w.Header().Add("Location", target)
			w.WriteHeader(303)
		} else {
			// todo send capnproto authed
		}
	}
}

func login(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]

	defer recovery(acceptLanguages, mimetype, w)

	if r.Method == "" || r.Method == "GET" {
		loginGet(w, r, acceptLanguages, mimetype)
	} else if r.Method == "POST" {
		loginPost(w, r, acceptLanguages, mimetype)
	}
}

func signupGet(w http.ResponseWriter, r *http.Request, acceptLanguages, mimetype string) {
	auth := getAuthToken(r)
	user, _ := libamuse.VerifyAuthToken(auth)
	host := r.Host
	if !user.IsEmpty() {
		w.Header().Add("Location", "/")
		w.WriteHeader(303)
		return
	}
	signup, err := libamuse.ShowSignup(acceptLanguages, mimetype, nil, false, "", "", host)
	render(signup, err, w, acceptLanguages, mimetype)
}

func signupPost(w http.ResponseWriter, r *http.Request, acceptLanguages, mimetype string) {
	// todo check mimetype (html,capnproto)

	if !config.OpenRegistration {
		err := errors.New("423")
		render("", err, w, acceptLanguages, mimetype)
		return
	}

	r.ParseForm()
	username := r.PostForm.Get("username")
	password := r.PostForm.Get("password")
	passwordConfirm := r.PostForm.Get("password2")
	sfaEnabled := r.PostForm.Get("sfaEnabled") == "true"
	sfaSecret := r.PostForm.Get("sfaSecret")
	sfa := r.PostForm.Get("sfa")
	host := r.Host

	recoveryCodes, err := libamuse.DoSignup(username, password, passwordConfirm, sfaEnabled, sfaSecret, sfa)
	if err != nil {
		fmt.Println(err)
		if authErr, ok := err.(accounts.AuthError); ok {
			var signup string
			var err error
			if mimetype == "text/html" {
				signup, err = libamuse.ShowSignup(acceptLanguages, mimetype, &authErr, sfaEnabled, sfaSecret, username, host)
			} else {
				// todo send capnproto not authed
			}
			render(signup, err, w, acceptLanguages, mimetype)
		} else {
			render("", err, w, acceptLanguages, mimetype)
		}
	} else {
		if mimetype == "text/html" {
			w.Header().Add("Location", "/signedup?recoveryCodes="+recoveryCodes)
			w.WriteHeader(303)
		} else {
			// todo send capnproto authed
		}
	}
}

func signup(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]

	defer recovery(acceptLanguages, mimetype, w)

	if r.Method == "" || r.Method == "GET" {
		signupGet(w, r, acceptLanguages, mimetype)
	} else if r.Method == "POST" {
		signupPost(w, r, acceptLanguages, mimetype)
	}
}

func signedup(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	recoveryCodes := r.URL.Query().Get("recoveryCodes")

	defer recovery(acceptLanguages, mimetype, w)

	signedup, err := libamuse.ShowSignedup(acceptLanguages, mimetype, recoveryCodes)
	render(signedup, err, w, acceptLanguages, mimetype)
}

func static(w http.ResponseWriter, r *http.Request) {
	etagReq := r.Header.Get("If-None-Match")
	f, err := os.Open(config.DataHome + "/" + r.URL.Path[1:])
	if err != nil {
		w.WriteHeader(500)
		return
	}
	defer f.Close()
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		w.WriteHeader(500)
		return
	}
	etag := base64.StdEncoding.EncodeToString(h.Sum(nil))

	mime.AddExtensionType(".woff2", "font/woff2")

	w.Header().Set("ETag", etag)
	if etagReq == etag {
		w.WriteHeader(304)
	} else {
		s := strings.Split(f.Name(), ".")
		ext := "." + s[len(s)-1]
		mimetype := mime.TypeByExtension(ext)
		w.Header().Set("Content-Type", mimetype+"; charset=utf-8")
		f.Seek(0, 0)
		if _, err := io.Copy(w, f); err != nil {
			w.WriteHeader(500)
		}
	}
}

func user(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages, mimetype string) {
	// todo user profile
	renderError(404, w, nil, acceptLanguages, mimetype)
}

func userAvatar(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) {
	etagReq := r.Header.Get("If-None-Match")
	r.ParseForm()
	size := r.Form.Get("size")
	avatar, err := libamuse.ShowUserAvatar(username, etagReq, auth, size == "small")
	if err != nil {
		render("", err, w, acceptLanguages, mimetype)
	}
	if string(avatar.Data) == "" {
		w.WriteHeader(304)
		return
	}
	w.Header().Set("Content-Type", avatar.Mimetype)
	w.Header().Set("ETag", avatar.Etag)
	w.Write(avatar.Data)
}

func addToWantlist(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages, mimetype string) {
	r.ParseForm()
	itemId := r.PostForm.Get("itemId")
	itemType := r.PostForm.Get("itemType")
	target := "/" + itemType + "s/" + itemId
	err := libamuse.AddToWantlist(username, auth, itemId, itemType, acceptLanguages, mimetype)
	if err != nil {
		render("", err, w, acceptLanguages, mimetype)
	} else {
		w.Header().Add("Location", target)
		w.WriteHeader(303)
	}
}

func userWatchlist(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) {
	if r.Method == "" || r.Method == "GET" {
		var page int
		r.ParseForm()
		filter := r.Form.Get("filter")
		fmt.Sscanf(r.Form.Get("page"), "%d", &page)
		watchlist, err := libamuse.ShowWatchlist(username, auth, acceptLanguages, mimetype, filter, page)
		render(watchlist, err, w, acceptLanguages, mimetype)
	} else if r.Method == "POST" {
		addToWantlist(w, r, username, auth, acceptLanguages, mimetype)
	}
}

func userTvQueue(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) {
	if r.Method == "" || r.Method == "GET" {
		var page int
		r.ParseForm()
		filter := r.Form.Get("filter")
		fmt.Sscanf(r.Form.Get("page"), "%d", &page)
		tvQueue, err := libamuse.ShowTvQueue(username, auth, acceptLanguages, mimetype, filter, page)
		render(tvQueue, err, w, acceptLanguages, mimetype)
	} else if r.Method == "POST" {
		r.ParseForm()
		itemId := r.PostForm.Get("itemId")
		itemType := r.PostForm.Get("itemType")
		masterItemId := strings.Split(itemId, "/")[0]
		target := "/" + itemType + "s/" + masterItemId
		err := libamuse.AddToWantlist(username, auth, itemId, itemType, acceptLanguages, mimetype)
		if err != nil {
			render("", err, w, acceptLanguages, mimetype)
		} else {
			w.Header().Add("Location", target)
			w.WriteHeader(303)
		}
	}
}

func userReadlist(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) {
	if r.Method == "" || r.Method == "GET" {
		var page int
		r.ParseForm()
		filter := r.Form.Get("filter")
		fmt.Sscanf(r.Form.Get("page"), "%d", &page)
		readlist, err := libamuse.ShowReadlist(username, auth, acceptLanguages, mimetype, filter, page)
		render(readlist, err, w, acceptLanguages, mimetype)
	} else if r.Method == "POST" {
		addToWantlist(w, r, username, auth, acceptLanguages, mimetype)
	}
}

func userExperiences(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) {
	if r.Method == "" || r.Method == "GET" {
		var page int
		r.ParseForm()
		filter := r.Form.Get("filter")
		fmt.Sscanf(r.Form.Get("page"), "%d", &page)
		experiences, err := libamuse.ShowExperiences(username, auth, acceptLanguages, mimetype, filter, page)
		render(experiences, err, w, acceptLanguages, mimetype)
	} else if r.Method == "POST" {
		r.ParseForm()
		itemId := r.PostForm.Get("itemId")
		itemType := r.PostForm.Get("itemType")
		isOtherTime := r.PostForm.Get("isOtherTime") == "true"

		var datetime string
		if isOtherTime {
			date := r.PostForm.Get("experiencedDate")
			time := r.PostForm.Get("experiencedTime")
			datetime = date + "T" + time + ":00"
		} else {
			datetime = ""
		}

		masterItemId := strings.Split(itemId, "/")[0]
		target := "/" + itemType + "s/" + masterItemId
		err := libamuse.AddToExperiences(username, auth, itemId, itemType, datetime, acceptLanguages, mimetype)
		if err != nil {
			render("", err, w, acceptLanguages, mimetype)
		} else {
			w.Header().Add("Location", target)
			w.WriteHeader(303)
		}
	}
}

func sessionDelete(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, session, acceptLanguages, mimetype string) {
	err := libamuse.SessionDelete(username, auth, session, acceptLanguages, mimetype)
	if err != nil {
		render("", err, w, acceptLanguages, mimetype)
	} else {
		w.Header().Add("Location", "/loggedout")
		w.WriteHeader(303)
	}
}

func userSessions(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages, mimetype string) {
	path := strings.Split(r.URL.Path[1:], "/")
	if len(path) == 3 {
		// todo show sessions
		renderError(404, w, nil, acceptLanguages, mimetype)
	} else if len(path) == 4 {
		if r.Method == "POST" {
			r.ParseForm()
			method := r.PostForm.Get("method")
			session := path[3]
			if method == "DELETE" {
				sessionDelete(w, r, username, auth, session, acceptLanguages, mimetype)
			}
		} else if r.Method == "DELETE" {
			session := path[3]
			sessionDelete(w, r, username, auth, session, acceptLanguages, mimetype)
		}
	}
}

func userRouter(w http.ResponseWriter, r *http.Request) {
	path := strings.Split(r.URL.Path[1:], "/")
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
	auth := getAuthToken(r)
	defer recovery(acceptLanguages, mimetype, w)

	if path[1] == "" {
		renderError(404, w, nil, acceptLanguages, mimetype)
		return
	}

	username := path[1]
	if len(path) == 2 {
		user(w, r, username, auth, acceptLanguages, mimetype)
	} else {
		switch path[2] {
		case "avatar":
			userAvatar(w, r, username, auth, acceptLanguages, mimetype)
		case "watchlist":
			userWatchlist(w, r, username, auth, acceptLanguages, mimetype)
		case "tvqueue":
			userTvQueue(w, r, username, auth, acceptLanguages, mimetype)
		case "readlist":
			userReadlist(w, r, username, auth, acceptLanguages, mimetype)
		case "experiences":
			userExperiences(w, r, username, auth, acceptLanguages, mimetype)
		case "sessions":
			userSessions(w, r, username, auth, acceptLanguages, mimetype)
		default:
			renderError(404, w, nil, acceptLanguages, mimetype)
		}
	}
}

func loggedout(w http.ResponseWriter, r *http.Request) {
	acceptLanguages := r.Header.Get("Accept-Language")
	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]

	defer recovery(acceptLanguages, mimetype, w)

	loggedout, err := libamuse.ShowLoggedOut(acceptLanguages, mimetype)
	setAuthCookie(false, "", w)
	render(loggedout, err, w, acceptLanguages, mimetype)
}

func route(port uint) {
	address := fmt.Sprintf("%s:%d", config.Address, port)

	http.HandleFunc("/", index)
	http.HandleFunc("/static/", static)
	http.HandleFunc("/about", about)
	http.HandleFunc("/items/", search)
	http.HandleFunc("/films/", film)
	http.HandleFunc("/tvseries/", tvSerie)
	http.HandleFunc("/persons/", person)
	http.HandleFunc("/books/", book)
	http.HandleFunc("/bookseries/", bookSerie)
	http.HandleFunc("/users/", userRouter)

	http.HandleFunc("/login", login)
	http.HandleFunc("/signup", signup)
	http.HandleFunc("/signedup", signedup)
	http.HandleFunc("/loggedout", loggedout)
	fmt.Printf("running on %s\n", address)
	e := http.ListenAndServe(address, nil)
	if e != nil {
		fmt.Println(e)
	}
}

func getTarget(referer, host string) string {
	url, err := url.Parse(referer)
	if err != nil {
		fmt.Println(err)
		return "/"
	}
	target := url.EscapedPath()
	if target == "" || url.Host != host {
		target = "/"
	}
	return target
}

func setAuthCookie(remember bool, token string, w http.ResponseWriter) {
	cookie := http.Cookie{
		Name: "auth", Value: token, HttpOnly: true,
		//SameSite: http.SameSiteStrictMode, Secure: true,  // note turn on in prod (https)
	}
	if remember {
		cookie.Expires = time.Now().Add(1000000000 * 60 * 60 * 24 * 30)
	} else {
		cookie.Expires = time.Now().Add(1000000000 * 60 * 60 * 24)
	}
	http.SetCookie(w, &cookie)
}

func getAuthToken(r *http.Request) accounts.Authentication {
	cookie, err := r.Cookie("auth")
	if err == nil {
		return accounts.Authentication{
			Token: cookie.Value,
		}
	}
	return accounts.Authentication{
		Token: r.Header.Get("Authorization"),
	}
}

func recovery(languages, mimetype string, w http.ResponseWriter) {
	if r := recover(); r != nil {
		renderError(500, w, errors.New(r.(string)), languages, mimetype)
	}
}

func render(result string, e error, w http.ResponseWriter, languages, mimetype string) {
	if e != nil {
		fmt.Println(e)
		if _, ok := e.(front.NoSuchRendererError); ok {
			renderError(406, w, e, languages, mimetype)
		} else if httpError, ok := e.(network.HttpError); ok {
			renderError(httpError.Status, w, httpError, languages, mimetype)
		} else if _, ok := e.(db.EmptyError); ok {
			renderError(410, w, e, languages, mimetype)
		} else if authError, ok := e.(accounts.AuthError); ok {
			if authError.Err.Error() == "401" {
				w.Header().Add("WWW-Authenticate", "Bearer")
				renderError(401, w, e, languages, mimetype)
			} else {
				renderError(403, w, e, languages, mimetype)
			}
		} else if e.Error() == "423" {
			renderError(423, w, e, languages, mimetype)
		} else {
			renderError(500, w, e, languages, mimetype)
		}
	} else {
		fmt.Fprint(w, result)
	}
}

func renderError(code int, w http.ResponseWriter, e error, languages, mimetype string) {
	w.WriteHeader(code)
	if code != 406 {
		errorPage, err := libamuse.ShowErrorPage(code, languages, mimetype)
		if err != nil {
			fmt.Fprintf(w, "Fatal error while rendering error %d.\nContact admin.", code)
		}
		fmt.Fprint(w, errorPage)
	}
}
